You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
644 lines
16 KiB
644 lines
16 KiB
<script setup lang="ts">
|
|
import type { AgentTitleStrategy } from "#server/service/agent/agent";
|
|
|
|
definePageMeta({
|
|
layout: false,
|
|
});
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const { $toast } = useNuxtApp() as any;
|
|
|
|
const slugParam = computed(() => String(route.params.slug));
|
|
const isCreate = computed(() => slugParam.value === "new");
|
|
|
|
interface ModelOption {
|
|
id: number;
|
|
name: string;
|
|
modelId: string;
|
|
providerName?: string;
|
|
providerId?: number;
|
|
supportsTools?: number;
|
|
}
|
|
|
|
interface ToolOption {
|
|
id: string;
|
|
slug: string;
|
|
name: string;
|
|
description: string;
|
|
}
|
|
|
|
const models = ref<ModelOption[]>([]);
|
|
const availableTools = ref<ToolOption[]>([]);
|
|
|
|
const form = reactive({
|
|
slug: "",
|
|
name: "",
|
|
description: "",
|
|
systemPrompt: "",
|
|
defaultModelId: null as number | null,
|
|
titleModelId: null as number | null,
|
|
titleStrategy: "llm" as AgentTitleStrategy,
|
|
enableThinking: 0,
|
|
enableTools: 1,
|
|
maxStepCount: 6,
|
|
isDefault: 0,
|
|
isCallable: 1,
|
|
sortOrder: 0,
|
|
enabled: 1,
|
|
toolIds: [] as string[],
|
|
});
|
|
|
|
const loading = ref(false);
|
|
const saving = ref(false);
|
|
|
|
async function loadModels() {
|
|
try {
|
|
const res = await $fetch<{ code: number; data: { list: any[] } }>("/api/llm/models");
|
|
const list = res.data?.list ?? [];
|
|
models.value = list.map((m) => ({
|
|
id: m.id,
|
|
name: m.name,
|
|
modelId: m.modelId,
|
|
providerName: m.providerName,
|
|
providerId: m.providerId,
|
|
supportsTools: m.supportsTools,
|
|
}));
|
|
} catch {
|
|
models.value = [];
|
|
}
|
|
}
|
|
|
|
async function loadTools() {
|
|
try {
|
|
const res = await $fetch<{ code: number; data: any[] }>("/api/agent-tools");
|
|
availableTools.value = (res.data ?? []).map((t: any) => ({
|
|
id: t.id,
|
|
slug: t.slug,
|
|
name: t.name,
|
|
description: t.description ?? "",
|
|
}));
|
|
} catch {
|
|
availableTools.value = [];
|
|
}
|
|
}
|
|
|
|
async function loadAgent() {
|
|
if (isCreate.value) return;
|
|
loading.value = true;
|
|
try {
|
|
const res = await $fetch<{ code: number; data: any }>(
|
|
`/api/admin/agents/${slugParam.value}`,
|
|
);
|
|
const d = res.data;
|
|
form.slug = d.slug ?? "";
|
|
form.name = d.name ?? "";
|
|
form.description = d.description ?? "";
|
|
form.systemPrompt = d.systemPrompt ?? "";
|
|
form.defaultModelId = d.defaultModelId ?? null;
|
|
form.titleModelId = d.titleModelId ?? null;
|
|
form.titleStrategy = (d.titleStrategy as AgentTitleStrategy) ?? "llm";
|
|
form.enableThinking = d.enableThinking ?? 0;
|
|
form.enableTools = d.enableTools ?? 1;
|
|
form.maxStepCount = d.maxStepCount ?? 6;
|
|
form.isDefault = d.isDefault ?? 0;
|
|
form.isCallable = d.isCallable ?? 1;
|
|
form.sortOrder = d.sortOrder ?? 0;
|
|
form.enabled = d.enabled ?? 1;
|
|
form.toolIds = (d.tools ?? []).map((t: any) => t.id);
|
|
} catch (e: any) {
|
|
$toast?.error?.(e?.data?.message ?? "加载 agent 失败");
|
|
router.push("/admin/agents");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function toggleTool(id: string) {
|
|
const idx = form.toolIds.indexOf(id);
|
|
if (idx >= 0) {
|
|
form.toolIds.splice(idx, 1);
|
|
} else {
|
|
form.toolIds.push(id);
|
|
}
|
|
}
|
|
|
|
async function handleSave() {
|
|
if (!form.slug || !form.name || !form.systemPrompt) {
|
|
$toast?.error?.("slug、name、systemPrompt 为必填");
|
|
return;
|
|
}
|
|
saving.value = true;
|
|
try {
|
|
const body = { ...form };
|
|
if (isCreate.value) {
|
|
await $fetch("/api/agents", { method: "POST", body });
|
|
$toast?.success?.("创建成功");
|
|
} else {
|
|
await $fetch(`/api/agents/${slugParam.value}`, { method: "PUT", body });
|
|
$toast?.success?.("保存成功");
|
|
}
|
|
router.push("/admin/agents");
|
|
} catch (e: any) {
|
|
$toast?.error?.(e?.data?.message ?? "保存失败");
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([loadModels(), loadTools()]);
|
|
await loadAgent();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="admin-agent-edit">
|
|
<header class="page-header">
|
|
<NuxtLink to="/admin/agents" class="back-link">← 返回列表</NuxtLink>
|
|
<h1 class="page-title">{{ isCreate ? '创建 Agent' : '编辑 Agent' }}</h1>
|
|
</header>
|
|
|
|
<div v-if="loading" class="loading-state">加载中...</div>
|
|
|
|
<template v-else>
|
|
<!-- 基本信息 -->
|
|
<section class="config-section">
|
|
<h2 class="section-title">基本信息</h2>
|
|
<p class="section-desc">Agent 的标识与显示名称</p>
|
|
<div class="form-grid">
|
|
<div class="form-field">
|
|
<label class="field-label">Slug <span class="required">*</span></label>
|
|
<input
|
|
v-model="form.slug"
|
|
type="text"
|
|
class="config-input"
|
|
placeholder="唯一标识,如 my-agent"
|
|
:disabled="!isCreate"
|
|
/>
|
|
</div>
|
|
<div class="form-field">
|
|
<label class="field-label">名称 <span class="required">*</span></label>
|
|
<input
|
|
v-model="form.name"
|
|
type="text"
|
|
class="config-input"
|
|
placeholder="显示名称"
|
|
/>
|
|
</div>
|
|
<div class="form-field form-field-full">
|
|
<label class="field-label">描述</label>
|
|
<input
|
|
v-model="form.description"
|
|
type="text"
|
|
class="config-input"
|
|
placeholder="简短描述"
|
|
/>
|
|
</div>
|
|
<div class="form-field">
|
|
<label class="field-label">排序</label>
|
|
<input
|
|
v-model.number="form.sortOrder"
|
|
type="number"
|
|
class="config-input"
|
|
placeholder="0"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 系统提示词 -->
|
|
<section class="config-section">
|
|
<h2 class="section-title">系统提示词 <span class="required">*</span></h2>
|
|
<p class="section-desc">引导模型行为的系统提示词</p>
|
|
<textarea
|
|
v-model="form.systemPrompt"
|
|
class="config-textarea"
|
|
rows="8"
|
|
placeholder="输入系统提示词..."
|
|
/>
|
|
</section>
|
|
|
|
<!-- 模型配置 -->
|
|
<section class="config-section">
|
|
<h2 class="section-title">模型配置</h2>
|
|
<p class="section-desc">默认对话模型与标题生成模型</p>
|
|
<div class="model-row">
|
|
<div class="model-field">
|
|
<label class="field-label">默认模型</label>
|
|
<select v-model="form.defaultModelId" class="config-select">
|
|
<option :value="null">— 未设置 —</option>
|
|
<option v-for="m in models" :key="m.id" :value="m.id">
|
|
{{ m.name }} ({{ m.modelId }}) — {{ m.providerName || '未知' }}{{ m.supportsTools ? ' · 支持工具' : '' }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="model-field">
|
|
<label class="field-label">标题生成模型</label>
|
|
<select v-model="form.titleModelId" class="config-select">
|
|
<option :value="null">— 未设置 —</option>
|
|
<option v-for="m in models" :key="m.id" :value="m.id">
|
|
{{ m.name }} ({{ m.modelId }}) — {{ m.providerName || '未知' }}{{ m.supportsTools ? ' · 支持工具' : '' }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<p v-if="models.length === 0" class="field-hint" style="color: var(--color-warning, #b06000);">
|
|
暂无可用模型,请先在 <NuxtLink to="/settings/llm-config" class="inline-link">LLM 配置</NuxtLink> 中添加 Provider 和模型
|
|
</p>
|
|
<div class="form-field" style="margin-top: 16px;">
|
|
<label class="field-label">标题策略</label>
|
|
<select v-model="form.titleStrategy" class="config-select">
|
|
<option value="llm">LLM 生成</option>
|
|
<option value="first-line">取首行</option>
|
|
<option value="none">不生成</option>
|
|
</select>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 行为配置 -->
|
|
<section class="config-section">
|
|
<h2 class="section-title">行为配置</h2>
|
|
<p class="section-desc">控制 Agent 的工具调用与思考能力</p>
|
|
<div class="toggle-grid">
|
|
<label class="toggle-item">
|
|
<input type="checkbox" :checked="form.enableTools === 1" @change="form.enableTools = form.enableTools ? 0 : 1" />
|
|
<div class="toggle-info">
|
|
<div class="toggle-name">启用工具</div>
|
|
<div class="toggle-desc">允许 Agent 调用绑定的工具</div>
|
|
</div>
|
|
</label>
|
|
<label class="toggle-item">
|
|
<input type="checkbox" :checked="form.enableThinking === 1" @change="form.enableThinking = form.enableThinking ? 0 : 1" />
|
|
<div class="toggle-info">
|
|
<div class="toggle-name">启用思考</div>
|
|
<div class="toggle-desc">允许模型使用思考模式</div>
|
|
</div>
|
|
</label>
|
|
<label class="toggle-item">
|
|
<input type="checkbox" :checked="form.isCallable === 1" @change="form.isCallable = form.isCallable ? 0 : 1" />
|
|
<div class="toggle-info">
|
|
<div class="toggle-name">可调用</div>
|
|
<div class="toggle-desc">在聊天页可选列表中显示</div>
|
|
</div>
|
|
</label>
|
|
<label class="toggle-item">
|
|
<input type="checkbox" disabled :checked="form.isDefault === 1" @change="form.isDefault = form.isDefault ? 0 : 1" />
|
|
<div class="toggle-info">
|
|
<div class="toggle-name">默认 Agent</div>
|
|
<div class="toggle-desc">设为新用户/未指定时的默认 Agent</div>
|
|
</div>
|
|
</label>
|
|
<label class="toggle-item">
|
|
<input type="checkbox" :checked="form.enabled === 1" @change="form.enabled = form.enabled ? 0 : 1" />
|
|
<div class="toggle-info">
|
|
<div class="toggle-name">启用</div>
|
|
<div class="toggle-desc">Agent 是否对用户可见</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<div class="form-field" style="margin-top: 16px;">
|
|
<label class="field-label">最大步数</label>
|
|
<input
|
|
v-model.number="form.maxStepCount"
|
|
type="number"
|
|
min="1"
|
|
max="50"
|
|
class="config-input"
|
|
style="width: 120px;"
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 工具绑定 -->
|
|
<section class="config-section">
|
|
<h2 class="section-title">工具绑定</h2>
|
|
<p class="section-desc">勾选此 Agent 可使用的工具(按勾选顺序提交)</p>
|
|
<div class="tools-list">
|
|
<label
|
|
v-for="t in availableTools"
|
|
:key="t.id"
|
|
class="tool-item"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
:checked="form.toolIds.includes(t.id)"
|
|
@change="toggleTool(t.id)"
|
|
/>
|
|
<div class="tool-info">
|
|
<div class="tool-name">{{ t.name }}</div>
|
|
<div class="tool-desc">{{ t.description }}</div>
|
|
</div>
|
|
</label>
|
|
<div v-if="availableTools.length === 0" class="empty-tools">
|
|
暂无可用工具
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 操作按钮 -->
|
|
<div class="action-bar">
|
|
<NuxtLink to="/admin/agents" class="btn-cancel">取消</NuxtLink>
|
|
<button class="save-btn" :disabled="saving" @click="handleSave">
|
|
{{ saving ? '保存中...' : (isCreate ? '创建' : '保存') }}
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.admin-agent-edit {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 32px 24px;
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.back-link {
|
|
display: inline-block;
|
|
font-size: 13px;
|
|
color: var(--color-muted);
|
|
text-decoration: none;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.back-link:hover {
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.inline-link {
|
|
color: var(--color-primary);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.field-hint {
|
|
font-size: 13px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.page-title {
|
|
font-family: var(--font-display);
|
|
font-size: 28px;
|
|
font-weight: 400;
|
|
color: var(--color-ink);
|
|
margin: 0;
|
|
}
|
|
|
|
.loading-state {
|
|
text-align: center;
|
|
padding: 48px;
|
|
color: var(--color-muted);
|
|
}
|
|
|
|
.config-section {
|
|
margin-bottom: 32px;
|
|
padding: 24px;
|
|
background: var(--color-surface-card);
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 16px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--color-ink);
|
|
margin: 0 0 4px;
|
|
}
|
|
|
|
.section-desc {
|
|
font-size: 13px;
|
|
color: var(--color-muted);
|
|
margin: 0 0 16px;
|
|
}
|
|
|
|
.required {
|
|
color: #e53e3e;
|
|
}
|
|
|
|
.form-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
}
|
|
|
|
.form-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.form-field-full {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.field-label {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--color-muted);
|
|
}
|
|
|
|
.config-input {
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
color: var(--color-ink);
|
|
background: var(--color-canvas);
|
|
outline: none;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.config-input:focus {
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.config-input:disabled {
|
|
background: var(--color-surface-soft);
|
|
color: var(--color-muted);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.config-textarea {
|
|
width: 100%;
|
|
padding: 12px 14px;
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 10px;
|
|
font-family: "SF Mono", "Fira Code", monospace;
|
|
font-size: 13px;
|
|
line-height: 1.6;
|
|
color: var(--color-ink);
|
|
background: var(--color-canvas);
|
|
resize: vertical;
|
|
outline: none;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.config-textarea:focus {
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.config-select {
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
color: var(--color-ink);
|
|
background: var(--color-canvas);
|
|
outline: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.model-row {
|
|
display: flex;
|
|
gap: 16px;
|
|
}
|
|
|
|
.model-field {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.toggle-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.toggle-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
background: var(--color-surface-soft);
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.toggle-item:hover {
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.toggle-item input[type="checkbox"] {
|
|
margin-top: 2px;
|
|
accent-color: var(--color-primary);
|
|
}
|
|
|
|
.toggle-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.toggle-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-ink);
|
|
}
|
|
|
|
.toggle-desc {
|
|
font-size: 12px;
|
|
color: var(--color-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.tools-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.tool-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
background: var(--color-surface-soft);
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.tool-item:hover {
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.tool-item input[type="checkbox"] {
|
|
margin-top: 2px;
|
|
accent-color: var(--color-primary);
|
|
}
|
|
|
|
.tool-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.tool-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-ink);
|
|
}
|
|
|
|
.tool-desc {
|
|
font-size: 12px;
|
|
color: var(--color-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.empty-tools {
|
|
padding: 24px;
|
|
text-align: center;
|
|
font-size: 13px;
|
|
color: var(--color-muted);
|
|
}
|
|
|
|
.action-bar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 12px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.btn-cancel {
|
|
padding: 8px 20px;
|
|
background: var(--color-surface-card);
|
|
color: var(--color-body-strong);
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.btn-cancel:hover {
|
|
background: var(--color-surface-soft);
|
|
}
|
|
|
|
.save-btn {
|
|
padding: 8px 24px;
|
|
background: var(--color-primary);
|
|
color: var(--color-on-primary);
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.save-btn:hover:not(:disabled) {
|
|
background: var(--color-primary-active);
|
|
}
|
|
|
|
.save-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|
|
|